Skip to content

Feature/react query migration#1629

Open
jesusbalderramawgu wants to merge 22 commits intoopenedx:masterfrom
WGU-Open-edX:feature/react-query-migration
Open

Feature/react query migration#1629
jesusbalderramawgu wants to merge 22 commits intoopenedx:masterfrom
WGU-Open-edX:feature/react-query-migration

Conversation

@jesusbalderramawgu
Copy link

@jesusbalderramawgu jesusbalderramawgu commented Feb 10, 2026

Description

PR to migrate this repository from Redux to React-query and React context. It closes the issue

What changed?

React context was added for client side state maintaining backward compatibility with the currents components, while Redux was changed for react query for server state management.
As it was mentioned above, the functionality was kept with changes required in some components to make it work with the new hooks. The tests were updated as well after the migration.

How Has This Been Tested?

  • tests were updated and some new were added.
  • Manual testing in the different paths, verifying that everything loads properly, also checking validations, loaders, requests.
  • The flow to login and register were tested successfully, I was able to see the learner dashboard with current credentials and with new users registered.

Note:
Even though the unit tests pass, there are some things that I couldn't test manually, such as

  • forgot password: I didn't get the email, do I need to run something to make this work?
  • ThirdPartyAuth component: this component has a SocialAuthProviders component but I couldn't render it, not sure what I need to render this or maybe is deprecated?
    if someone knows how to test this, please let me know.

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Feb 10, 2026
@openedx-webhooks
Copy link

Thanks for the pull request, @jesusbalderramawgu!

This repository is currently maintained by @openedx/2u-infinity.

Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@jesusbalderramawgu
Copy link
Author

for some reason the lint is failing with the following files webpack.prod.config.js, .eslintrc.js and jest.config.js.
I will check this tomorrow

@jesusbalderramawgu
Copy link
Author

assign me

@mphilbrick211 mphilbrick211 added the mao-onboarding Reviewing this will help onboard devs from an Axim mission-aligned organization (MAO). label Feb 10, 2026
@mphilbrick211 mphilbrick211 moved this from Needs Triage to Waiting on Author in Contributions Feb 10, 2026
tsconfig.json Outdated
"outDir": "dist"
},
"include": ["*.js", ".eslintrc.js", "src/**/*", "plugins/**/*", "jest.config.ts"],
"exclude": ["*.js", ".eslintrc.js", "dist", "node_modules"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"exclude": ["*.js", ".eslintrc.js", "dist", "node_modules"]
"exclude": ["dist", "node_modules"]

This fixes the lint problems.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you Jacob, this fixed the issue

@codecov
Copy link

codecov bot commented Feb 10, 2026

Codecov Report

❌ Patch coverage is 87.99314% with 70 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.22%. Comparing base (9b7be2a) to head (e5619f6).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
...ofiling/components/ProgressiveProfilingContext.tsx 0.00% 22 Missing and 2 partials ⚠️
...on-components/components/ThirdPartyAuthContext.tsx 64.28% 10 Missing ⚠️
src/forgot-password/ForgotPasswordPage.jsx 80.76% 5 Missing ⚠️
src/login/data/apiHook.ts 85.71% 5 Missing ⚠️
src/register/components/RegisterContext.tsx 93.50% 5 Missing ⚠️
src/common-components/PasswordField.jsx 50.00% 4 Missing ⚠️
src/register/RegistrationPage.jsx 89.47% 4 Missing ⚠️
src/reset-password/data/apiHook.ts 90.69% 4 Missing ⚠️
src/reset-password/ResetPasswordPage.jsx 94.33% 3 Missing ⚠️
src/forgot-password/data/apiHook.ts 87.50% 2 Missing ⚠️
... and 3 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1629      +/-   ##
==========================================
+ Coverage   87.57%   91.22%   +3.65%     
==========================================
  Files         125      108      -17     
  Lines        2309     2347      +38     
  Branches      648      667      +19     
==========================================
+ Hits         2022     2141     +119     
+ Misses        278      199      -79     
+ Partials        9        7       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@arbrandes arbrandes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this work, Jesus! I like that tests are passing and things generally work. But beyond the inline comments and change requests I made, there are a few things to address:

1. Error responses don't show

The error responses don't seem to be rendered for the user. Take this one in master, for example:

Image

In this branch the error response is simply not shown.

2. Some patterns are not consistent

I focused mostly on the different apiHooks.ts files: each one seems to do things slightly differently. While there is no Open edX guideline for useMutation() calls, we should pick one pattern and stick with it at least across each repository. It makes things easier to maintain. See the inline comments.

It's possible this applies elsewhere and I didn't catch it. But please keep the concept in mind.

3. Remove dead code

Please remove all dead code - basically, everything Redux - in this PR. There isn't much advantage in creating a separate PR for that and leaving a bunch of commented-out stuff in. It's fine if it's a separate commit, though.

4. Types

I'm not going to block the PR on it because the focus is on react-query, not Typescript, but wherever possible it would be much better if we replaced any with actual types, even if it's just the primitive ones. At the very least we should create a follow-up ticket to clean those up.

Again, thanks for the hard work!

setBannerEmail(emailUsed);
setFormErrors('');
},
onError: () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to declare the error parameter:

Suggested change
onError: () => {
onError: (error) => {

Comment on lines 6 to 9
error: string | null;
setError: (error: string | null) => void;
isLoading: boolean;
setIsLoading: (isLoading: boolean) => void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation of the context value below is completely different from this type. Please fix it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this was here before your PR, but this is as good an opportunity to fix it as any. If we don't add a dependency array, this will request a new CSRF token on every render:

Suggested change
}, []);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see this, good catch. Fixed!

});

const registrationErrorCode = registrationError?.errorCode || backendRegistrationError?.errorCode;
const submitState = registrationMutation.isLoading ? PENDING_STATE : DEFAULT_STATE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure we don't mean isPending, here? See the react-query v5 migration blurb. Just checking.

Suggested change
const submitState = registrationMutation.isLoading ? PENDING_STATE : DEFAULT_STATE;
const submitState = registrationMutation.isPending ? PENDING_STATE : DEFAULT_STATE;

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah you are right I changed this

@@ -0,0 +1,35 @@
// TODO: Delete this file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please go ahead and delete any dead code.

@@ -0,0 +1,53 @@
import { camelCaseObject } from '@edx/frontend-platform';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please name this file with the same pattern as the other ones, apiHook.ts.

mutationFn: async (loginData: LoginData) => {
try {
return await login(loginData);
} catch (error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason you're not using the onError pattern (which you do use elsewhere)?


const useRegistration = (options = {}) => useMutation({
mutationFn: (registrationPayload) => registerNewUserApi(registrationPayload),
onSuccess: (data) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you are using onSuccess and onError outside the mutationFn, which is different to how you do it elsewhere. We should settle on a pattern and use it consistently everywhere, for ease of maintenance.

import { getFieldsValidations, registerNewUserApi } from './api.ts';
import { INTERNAL_SERVER_ERROR } from './constants';

const useRegistration = (options = {}) => useMutation({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a type to the options, like you do for progressive-profiling.

));
expect(store.dispatch).not.toHaveBeenCalledWith(loginRequest({}));
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
expect(props.loginRequest).not.toHaveBeenCalled();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't be testing the Redux stuff, anymore.

@arbrandes
Copy link
Contributor

Ah, and I forgot one thing: please try not to reduce coverage, both at the diff level and project level.

@jesusbalderramawgu
Copy link
Author

Thank you for this work, Jesus! I like that tests are passing and things generally work. But beyond the inline comments and change requests I made, there are a few things to address:

1. Error responses don't show

The error responses don't seem to be rendered for the user. Take this one in master, for example:

Image In this branch the error response is simply not shown.

2. Some patterns are not consistent

I focused mostly on the different apiHooks.ts files: each one seems to do things slightly differently. While there is no Open edX guideline for useMutation() calls, we should pick one pattern and stick with it at least across each repository. It makes things easier to maintain. See the inline comments.

It's possible this applies elsewhere and I didn't catch it. But please keep the concept in mind.

3. Remove dead code

Please remove all dead code - basically, everything Redux - in this PR. There isn't much advantage in creating a separate PR for that and leaving a bunch of commented-out stuff in. It's fine if it's a separate commit, though.

4. Types

I'm not going to block the PR on it because the focus is on react-query, not Typescript, but wherever possible it would be much better if we replaced any with actual types, even if it's just the primitive ones. At the very least we should create a follow-up ticket to clean those up.

Again, thanks for the hard work!

Thank you Adolfo for your feedback, I pushed more changes to the branch.

Thank you for this work, Jesus! I like that tests are passing and things generally work. But beyond the inline comments and change requests I made, there are a few things to address:

1. Error responses don't show

The error responses don't seem to be rendered for the user. Take this one in master, for example:

Image In this branch the error response is simply not shown.

2. Some patterns are not consistent

I focused mostly on the different apiHooks.ts files: each one seems to do things slightly differently. While there is no Open edX guideline for useMutation() calls, we should pick one pattern and stick with it at least across each repository. It makes things easier to maintain. See the inline comments.

It's possible this applies elsewhere and I didn't catch it. But please keep the concept in mind.

3. Remove dead code

Please remove all dead code - basically, everything Redux - in this PR. There isn't much advantage in creating a separate PR for that and leaving a bunch of commented-out stuff in. It's fine if it's a separate commit, though.

4. Types

I'm not going to block the PR on it because the focus is on react-query, not Typescript, but wherever possible it would be much better if we replaced any with actual types, even if it's just the primitive ones. At the very least we should create a follow-up ticket to clean those up.

Again, thanks for the hard work!

Thank you Adolfo for your feedback, I pushed more changes to this branch.
the error responses were not correctly on the login but in the registration page were correct, not sure if I'm doing the same as you.
The loginPage was wrong I wasn't matching correctly the errorCode.type that is used in the component and it wasn't showing the proper errors, now this is fixed.

I refactored the apiHooks and renamed the ones that were incorrect.

I have deleted the redux code!

I have added types to the ones that I know what I expect.

Also I made extra changes removing unnecessary code.

Anything please let me know and again thank you so much!

Screenshot 2026-02-11 at 10 15 13 p m Screenshot 2026-02-11 at 10 15 43 p m

@jesusbalderramawgu
Copy link
Author

Ah, and I forgot one thing: please try not to reduce coverage, both at the diff level and project level.

thanks for this comment, yeah I fixed the existing ones and added new ones for the new code, but also I deleted the ones related to redux. I will check this tomorrow and I will add more tests to get the correct coverage

@jesusbalderramawgu jesusbalderramawgu force-pushed the feature/react-query-migration branch from bb02894 to d999091 Compare February 12, 2026 04:26
Copy link
Contributor

@arbrandes arbrandes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I can confirm the error handling bug is fixed, thanks! Thank you for addressing my comments, otherwise: the hooks refactor looks great!

There are still changes to make, however. For instance:

  • Please don't disable the exhaustive-deps linting rules. I know there are some stable objects that wouldn't change, but better to be explicit than implicit. If the dependencies are too big, then the contexts need to be split up.

package.json Outdated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the redux dependencies as well. There might be others I didn't mark: if so, remove them too.

src/MainApp.jsx Outdated

registerIcons();

const queryClient = new QueryClient();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auth-related mutations (login, registration) probably should not retry on failure — a failed login retried 3 times would be confusing. Maybe something like:

Suggested change
const queryClient = new QueryClient();
const queryClient = new QueryClient({
defaultOptions: {
mutations: { retry: false },
},
});

setRegistrationResult,
setBackendCountryCode,
setRegistrationError,
// eslint-disable-next-line react-hooks/exhaustive-deps
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should not be necessary to disable any eslint warnings. If the context is too big, let's split it up. Let's try splitting into at least RegistrationFormContext and RegistrationValidationContext.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this, I'm just concern splitting this into two contexts right now, basically I just moved the redux state into react context, didn't make any refactor. I think we can make a refactor later maybe when I work on the frontend-base update. what do you think?
by now what I did was to do a mini refactor to remove some properties that are not required in the context and move it into the local state. Also I have the useReducer and useCallback getting stable references avoiding unnecessary re renders, now it is shorter and removed the eslint warning. Looks better

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, thanks!

const location = useLocation();

const registrationResponse = location.state?.registrationResult;
// const registrationResponse = location.state?.registrationResult;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's still some commented out stuff.

const userId = location.state?.userId;

const userCountry = useSelector((state) => state.register.backendCountryCode);
// const userCountry = useSelector((state) => state.register.backendCountryCode);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove reference to redux.

tsconfig.json Outdated
{
"extends": "@edx/typescript-config",
"compilerOptions": {
"baseUrl": "./src",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use baseUrl, please. Use rootDir instead.

setErrorCode({
type: INVALID_FORM,
count: prevState.count + 1,
count: errorCode.count + 1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not right. You cannot use errorCode directly, here. You're basically using a stale reference. You should, instead:

setErrorCode(prev => ({
  type: INVALID_FORM,
  count: prev.count + 1,
  context: {},
}));

onError: (formattedError) => {
setErrorCode({
type: formattedError.type,
count: errorCode.count + 1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same problem as on line 173. errorCode is a stale reference. Do something like:

setErrorCode(prev => ({
  type: formattedError.type,
  count: prev.count + 1,
  context: formattedError.context,
}));

@jesusbalderramawgu
Copy link
Author

Ok, I can confirm the error handling bug is fixed, thanks! Thank you for addressing my comments, otherwise: the hooks refactor looks great!

There are still changes to make, however. For instance:

  • Please don't disable the exhaustive-deps linting rules. I know there are some stable objects that wouldn't change, but better to be explicit than implicit. If the dependencies are too big, then the contexts need to be split up.

Thank you Adolfo for your comments, I addressed the changes that you requested also the code coverage was fixed.
Anything please let me know, Thanks!

Copy link
Contributor

@arbrandes arbrandes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for bearing with me! I found a few more things to change.

setValidationsFailure,
validationApiRateLimited,
clearRegistrationBackendError,
} = useRegisterContext();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context is not always available. For example, from ResetPasswordPage. If somebody clicks on a /password_reset_confirm/ link, this will throw an exception. The tests are only passing because they wrap the page with the provider, but this is not the case in the app itself.

In other words... probably use RegisterProvider in ResetPasswordPage.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it should be there, for some reason I deleted it. now I added the provider again.
But in the future we should refactor this PasswordField I guess to avoid dependency with this context and make it more generic

const { formatMessage } = useIntl();
const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth - 1 });
const {
registrationResult,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

registrationResult doesn't exist in useRegisterContext(). The user will always be redirected to the dashboard, below.

ProgressiveProfiling gets this from the route (via useLocation()). Maybe do the same here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this case I brought the registrationResult back to the register context, because it takes the url when a user registers and other data. that's why it was in the context in first place, I missed this last night.
Maybe this is a refactor for the next step

if (status === TOKEN_STATE.PENDING) {
if (token) {
props.validateToken(token);
validateResetToken(token, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't call this directly in the render function. It's going to be called on every render. This needs to be moved into a useEffect with the [token] as a dependency.

(There's some other similar weirdness on this page, but most of it was already there. This one is worth fixing now because it's using a react-query mutation.)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't notice, that it was directly. I made the change, thank you

};

Logistration.defaultProps = {
selectedPage: REGISTER_PAGE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this default, the register page in MainApp.jsx needs an explicit selectedPage, too.

src/MainApp.jsx Outdated
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
}
/>
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs an explicit selected page now that the default is gone:

Suggested change
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration selectedPage={REGISTER_PAGE} /></UnAuthOnlyRoute>} />

// Error constants
export const THIRD_PARTY_AUTH_ERROR = 'third-party-auth-error';

const useThirdPartyAuthHook = () => useMutation({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use a mutation for a GET request?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are right, I made the change

forgotPassword(email)
),
onSuccess: (data: ForgotPasswordResult, email: string) => {
logInfo(`Forgot password email sent to ${email}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't be logging user's emails. Was the code doing this before?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was my mistake I removed it

Comment on lines 242 to 246
@@ -207,18 +245,4 @@ ResetPasswordPage.defaultProps = {
errorMsg: null,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These aren't needed anymore, right?

status: null,
submitState: DEFAULT_STATE,
};
ForgotPasswordPage.defaultProps = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need defaultProps at all, no?

status: PropTypes.string,
submitState: PropTypes.string,
};
ForgotPasswordPage.propTypes = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this, anymore.

@jesusbalderramawgu
Copy link
Author

Thanks for bearing with me! I found a few more things to change.

Thank you Adolfo, I addressed the other comments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mao-onboarding Reviewing this will help onboard devs from an Axim mission-aligned organization (MAO). open-source-contribution PR author is not from Axim or 2U

Projects

Status: Waiting on Author

Development

Successfully merging this pull request may close these issues.

[react-query] Convert from redux to react-query

5 participants